package in.co.codedoc.event;

import in.co.codedoc.ioc.ClassUtil;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

@SuppressWarnings("unchecked")
public abstract class EventRegistryBase
{
	public synchronized void Register(Class eventType,Object handler)
	{
		Class handlerType = handler.getClass();
		
		for(Method method:handlerType.getMethods())
		{
			Class[] pts = method.getParameterTypes();
			EventHandler eventHandler = method.getAnnotation(EventHandler.class);
			if(eventHandler != null && pts[0].isAssignableFrom(eventType))
			{
				Register(eventType,handler,method,eventHandler.matchEventTypeExactly());
			}
		}
	}

	public synchronized void Register(Class eventType,Object handler,Method method)
	{
		Register(eventType, handler, method, false);
	}
	
	public synchronized void Register(Class eventType,Object handler,Method method,boolean matchEventTypeExactly)
	{
		List<EventTarget> targets = targetsByEventType.get(eventType);
		
		if(targets == null)
		{
			targetsByEventType.put(eventType,targets = new ArrayList<EventTarget>());
		}
		
		targets.add(new EventTarget(eventType,handler,method,matchEventTypeExactly));
	}
	
	public void Dispatch(Object event)
	{
		if(Logger.logger.isDebugEnabled())
		{
			Logger.logger.debug("Dispatching a " + event.getClass() + ":" + event);
		}
		
		Set<Class> eventTypes = ClassUtil.GetTypes(event.getClass());
		
		HashSet<EventTarget> gatheredTargets = new HashSet<EventTarget>();
		
		for(Class eventType:eventTypes)
		{
			List<EventTarget> targets = targetsByEventType.get(eventType);
			if(targets != null)
			{
				gatheredTargets.addAll(targets);
			}
		}
		
		for(EventTarget target:gatheredTargets)
		{
			if(target.matchEventTypeExactly)
			{
				if(!target.eventType.equals(event.getClass()))
				{
					continue;
				}
			}
			Dispatch(event, target);
		}
	}
	
	protected void Dispatch(Object event,EventTarget target)
	{
		if(Logger.logger.isDebugEnabled())
		{
			Logger.logger.debug("Dispatching to " + target.target.getClass() + " via " + target.method);
		}
		try
		{
			target.method.invoke(target.target, new Object[]{event});
		}
		catch(Throwable th)
		{
			throw new RuntimeException
			(
				"Exception while dispatching '" + event.getClass() +
				"' to '" + target.target.getClass() + "' via '" +
				target.method + "':" + th.getMessage(),
				th
			);
		}
	}

	protected static class EventTarget
	{
		public Class eventType;
		public Object target;
		public Method method;
		public boolean matchEventTypeExactly;
		
		public EventTarget(Class eventType,Object target,Method method, boolean matchEventTypeExactly)
		{
			this.eventType = eventType;
			this.target = target;
			this.method = method;
			this.matchEventTypeExactly = matchEventTypeExactly;
		}
		
		public boolean equals(Object t)
		{
			if(t == null || !(t instanceof EventTarget))
			{
				return false;
			}
			
			EventTarget that = (EventTarget)t;
			return (target == that.target) && method.equals(that.method);
		}
		
		public int hashCode()
		{
			return target.hashCode()^method.hashCode();
		}
	}
	
	private HashMap<Class,List<EventTarget>> targetsByEventType
		= new HashMap<Class, List<EventTarget>>();
}
